package com.shanxia.im.sdk.client;

import com.shanxia.codec.Decoder;
import com.shanxia.codec.ProtocolDataBase;
import com.shanxia.codec.cmd.*;
import com.shanxia.codec.enums.BatteryTypeEnum;
import com.shanxia.codec.enums.ChargeStartFailEnum;
import com.shanxia.im.sdk.client.coder.MessageDecoder;
import com.shanxia.im.sdk.client.coder.MessageEncoder;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.handler.timeout.IdleStateHandler;

import java.io.UnsupportedEncodingException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import io.netty.util.AttributeKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 给力的断线重连
 */
public class NettyClient {

    private final Logger logger = LoggerFactory.getLogger(NettyClient.class);
    private String host;
    private int port;

    private ExecutorService executor = Executors.newFixedThreadPool(1);

    /**
     * 计数器:客户端连续N次没有收到服务端的pong消息
     */
    private int unRecPongTimes = 0;
    private final int READ_IDLE_TIME = 150;//秒
    private final int CONNECT_TIMEOUT = 10 * 1000;//秒
    private final int HEARBEAT_TIME_OUT = (READ_IDLE_TIME + 20) * 1000;// 收到服务端心跳请求超时时间 毫秒
    private final String KEY_LAST_HEART_TIME = "KEY_LAST_HEART_TIME";

    /**
     * channel是否停止
     */
    private boolean isStop = false;

    private ChannelFuture future = null;

    private static ChannelFutureListener channelFutureListener;

    /**
     * 客户端连接的channel
     */
    private Channel channel = null;

    private EventLoopGroup group;

    private Bootstrap bootstrap;

    public NettyClient(String host, int port) {

        this.host = host;
        this.port = port;
        group = new NioEventLoopGroup();
        bootstrap = new Bootstrap();
        //设置TCP协议的属性
        bootstrap.group(group)
                .option(ChannelOption.SO_KEEPALIVE, true)
                .option(ChannelOption.TCP_NODELAY, true)
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, CONNECT_TIMEOUT)
                .channel(NioSocketChannel.class).handler(new HeartbeatInitializer());

        // 通过监听listener来做重连
        channelFutureListener = new ChannelFutureListener() {

            public void operationComplete(ChannelFuture cf) throws Exception {

                if (isConnected()) {
                    logger.info("...... 连接服务器成功......");

                    /** 此处非常关键,一定要做该处理,否则重连上之后只有通道无绑定信息 */
                    // 建立连接成功之后，登陆网关服务器[]
                    cbind();

                } else {
                    logger.info("...... 连接服务器失败......5秒后重新连接......");
                    // 5秒后重新连接
                    cf.channel().eventLoop().schedule(new Runnable() {
                        @Override
                        public void run() {
                            connServer();
                        }
                    }, 5, TimeUnit.SECONDS);
                }
            }
        };
    }

    /**
     * 让线程池执行 连接服务器 任务
     */
    public void connServer() {

        if (!isStop) {
            executor.execute(new Runnable() {
                public void run() {
                    syncConnection();
                }
            });
        }
    }

    /**
     * 客户端连接方法
     */
    private synchronized void syncConnection() {

        if (isConnected()) {
            return;
        }
        try {
            logger.info("**************** 正在连接服务器  " + host + ":" + port + "......");
            future = bootstrap.connect(host, port).syncUninterruptibly();
            channel = future.channel();
            // 重置计数器
            unRecPongTimes = 0;
            // 是否关闭
            isStop = false;

        } catch (Exception e) {

            logger.error("**************** 连接服务器失败出现异常  " + host + ":" + port + "......将在5秒后重新尝试连接");

        } finally {
            // 保证服务端断开，如果重连时 链接出现异常 也能够定时重连
            future.addListener(channelFutureListener);
        }
    }

    /**
     * 登陆bind
     *
     * @throws Exception
     */
    public void cbind() throws Exception {

        ByteBuf buff = this.channel.alloc().buffer();

        Cmd106 cmd106 = new Cmd106();
        cmd106.setSn("10000");
        cmd106.setSign(0);
        cmd106.setSoftwareVer("100.00");
        cmd106.setSignInterval(60);
        cmd106.setPileNum(1);
        cmd106.setChargingRecordCount(1);
        LocalDateTime localDateTime = LocalDateTime.now();
        DateTimeFormatter dtf2 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        cmd106.setCurrentTime(localDateTime.format(dtf2));// 设置模拟充电桩的标准时间

        buff.writeBytes(cmd106.getBytes());

        this.channel.writeAndFlush(buff);

    }

    public boolean isConnected() {
        if (channel == null) {
            return false;
        }
        return channel.isActive();
    }

    public void stop() {
        //是否关闭
        isStop = true;
        group.shutdownGracefully();
        if (channel != null && channel.isOpen()) {
            channel.close();
        }
    }

    public void destroy() {
        this.stop();
    }

    public class HeartbeatInitializer extends ChannelInitializer<SocketChannel> {

        @Override
        protected void initChannel(SocketChannel ch) throws Exception {

            ChannelPipeline pipeline = ch.pipeline();
            //设置分隔符解码器
            pipeline.addLast("decoder", new MessageDecoder());
            pipeline.addLast("encoder", new MessageEncoder());
            pipeline.addLast("ping", new IdleStateHandler(READ_IDLE_TIME, 0, 0, TimeUnit.SECONDS));
            // 客户端的逻辑
            pipeline.addLast("handler", new ClientHandler());
        }

    }

    public class ClientHandler extends SimpleChannelInboundHandler<Object> {

        @Override
        protected void channelRead0(ChannelHandlerContext ctx, Object data) throws Exception {

            logger.info("Server say : " + data);

            ProtocolDataBase protocolDataBase = (ProtocolDataBase) data;

            unRecPongTimes = 0;

            //收到服务端发来的心跳请求命令，则马上回应服务器
            if (protocolDataBase.getCmd() == 101) {

                System.out.println("......收到心跳，准备反馈心跳......");

                ByteBuf buff = ctx.alloc().buffer();
                Cmd102 cmd102 = new Cmd102();
                cmd102.setSn("10000");
                cmd102.setHeartSequence(0);
                buff.writeBytes(cmd102.getBytes());
                channel.writeAndFlush(buff);
            } else if (protocolDataBase.getCmd() == 3) {
                Cmd3 cmd3 = (Cmd3)protocolDataBase;
                Cmd4 cmd4 = new Cmd4();
                cmd4.setCode(1);
                cmd4.setSn("10000");
                cmd4.setType(cmd3.getType());
            } else if (protocolDataBase.getCmd() == 5) {
                System.out.println("......收到cmd5......");
            } else if (protocolDataBase.getCmd() == 1103) {
                System.out.println("......收到cmd1103......");
            } else {
                System.out.println("......收到其它指令......cmd="+protocolDataBase.getCmd());
            }

        }

        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {

            logger.info("......Client active ......");

            // 设置最近一次心跳时间
            setLastHeartbeatTime(ctx.channel());
            super.channelActive(ctx);
        }

        private void setLastHeartbeatTime(Channel channel) {
            channel.attr(AttributeKey.valueOf(KEY_LAST_HEART_TIME)).set(System.currentTimeMillis());
        }

        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {

            logger.info("...... channelInactive Client close ......");

            super.channelInactive(ctx);
            //重连接
            ctx.channel().eventLoop().schedule(new Runnable() {
                @Override
                public void run() {
                    connServer();
                }
            }, 5, TimeUnit.SECONDS);

        }

        public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {

            if (evt instanceof IdleStateEvent) {
                IdleStateEvent event = (IdleStateEvent) evt;
                if (event.state() == IdleState.READER_IDLE) {
                    //读超时
                    logger.info("===客户端===(READER_IDLE 读超时)");
                    Long lastTime = getLastHeartbeatTime(ctx.channel());
                    // 确保手机wifi网络切换出现通道无效的情况
                    if (lastTime != null && System.currentTimeMillis() - lastTime > HEARBEAT_TIME_OUT) {
                        ctx.channel().close();
                        logger.info("****************IM心跳超时 ,即将重新连接......" + " NID:" + ctx.channel().id().asShortText());
                    }
                } else if (event.state() == IdleState.WRITER_IDLE) {
                    //写超时
                    logger.info("===客户端===(WRITER_IDLE 写超时+1)");
                    if (unRecPongTimes < 3) {

                        ByteBuf buff = ctx.alloc().buffer();
                        Cmd102 cmd102 = new Cmd102();
                        cmd102.setSn("10000");
                        cmd102.setHeartSequence(0);
                        buff.writeBytes(cmd102.getBytes());
                        ctx.channel().writeAndFlush(buff);

                        unRecPongTimes++;
                    } else {
                        ctx.close();
                    }
                } else if (event.state() == IdleState.ALL_IDLE) {
                    //总超时
                    logger.info("===客户端===(ALL_IDLE 总超时)");
                }
            }
        }

        private Long getLastHeartbeatTime(Channel channel) {
            return (Long) channel.attr(AttributeKey.valueOf(KEY_LAST_HEART_TIME)).get();
        }
    }

    /**
     * @param args
     * @throws InterruptedException
     * @throws UnsupportedEncodingException
     */
    public static void main(String[] args) throws Exception {

        NettyClient client = new NettyClient("127.0.0.1", 23456);

        /** 1、第一步 连接服务器 */
        client.connServer();
        TimeUnit.SECONDS.sleep(3);
        /** 2、第二步 签到 */
        client.cbind();

        System.out.println("......测试客户端登录成功......");

        while (true) {

            TimeUnit.SECONDS.sleep(3);

            ByteBuf buff = client.channel.alloc().buffer();

            /*Cmd102 request = new Cmd102();
            request.setSn("10000");
            request.setHeartSequence(1);*/

            String CMD2_DATA = "AA F5 34 00 10 02 02 00 00 00 00 00 33 31 30 31 30 34 30 30 30 31 30 30 30 30 32 30 30 30 30 34 00 00 00 00 00 00 00 00 00 00 00 00 01 01 00 00 00 01 00 D5";
            //    String CMD2_DATA = "AA F5 34 00 10 03 02 00 00 00 00 00 33 31 30 31 30 34 30 30 30 31 30 30 30 30 32 30 30 30 30 34 00 00 00 00 00 00 00 00 00 00 00 00 01 15 00 00 00 01 00 E9";
            //    String CMD4_DATA = "AA F5 33 00 10 05 04 00 00 00 00 00 33 31 30 31 30 34 30 30 30 31 30 30 30 30 32 30 30 30 30 34 00 00 00 00 00 00 00 00 00 00 00 00 01 02 00 00 00 00 D7";

            //    String CMD3_DATA = "AA F5 1C 00 04 05 03 00 00 00 00 00 01 02 00 00 00 08 00 20 20 10 12 15 13 07 FF 9E";
            String CMD3_DATA = "AA F5 14 00 04 04 03 00 00 00 00 00 00 0B 00 00 00 00 00 0E";

            String CMD4_DATA = "AA F5 AF 00 10 04 04 00 00 00 00 00 33 31 30 31 30 34 30 30 30 31 30 30 30 30 32 30 30 30 30 34 00 00 00 00 00 00 00 00 00 00 00 00 00 0B 00 00 00 00 68 74 74 70 3A 2F 2F 75 62 69 64 79 2E 6C 69 6E 67 68 75 63 68 6F 6E 67 74 65 63 68 2E 63 6F 6D 2F 6C 68 63 5F 70 61 79 2F 6F 70 2F 61 2E 68 74 6D 3F 64 69 64 3D 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FA";

            String CMD5_DATA = "AA F5 19 00 04 00 05 00 00 00 00 00 01 0F 00 00 00 01 04 00 55 00 00 00 6F";
            //    String CMD5_DATA = "AA F5 19 00 10 01 05 00 11 22 33 44 01 02 00 00 00 01 04 00 55 00 00 00 0C";

            String CMD6_DATA = "AA F5 34 00 10 00 06 00 00 00 00 00 33 31 30 31 30 34 30 30 30 31 30 30 30 30 32 30 30 30 30 34 00 00 00 00 00 00 00 00 00 00 00 00 01 0F 00 00 00 01 00 E7";
            String CMD7_DATA = "AA F5 6C 00 10 0A 07 00 00 00 00 00 01 00 00 00 00 57 04 00 00 02 00 00 00 D0 07 00 00 20 17 06 28 16 10 00 FF 01 32 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 1F 20 1E 1F 26 1E 23 20 21 1F 21 21 21 21 24 1E 1F 1E 1E 1E 1E 1F 1E 1E 1E 1E 1E 1E 1F 1F 24 25 F3";
            String CMD8_DATA = "AA F5 52 00 10 0A 08 00 00 00 00 00 30 31 30 30 30 30 31 30 30 30 30 30 30 31 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 1F 20 1E 1F 26 1E 23 20 21 1F 21 21 21 21 24 1E 1F 1E 1E 1E 1E 1F 1E 1E 1E 1E 1E 1E 1F 1F 24 25 A6";
            String CMD101_DATA = "AA F5 0F 00 04 07 65 00 00 00 00 00 27 00 8C";
            String CMD102_DATA = "AA F5 3F 00 10 07 66 00 00 00 00 00 33 31 30 31 30 34 30 30 30 31 30 30 30 30 32 30 30 30 30 34 00 00 00 00 00 00 00 00 00 00 00 00 27 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 5D";
            String CMD103_DATA = "AA F5 0F 00 04 01 67 00 00 00 00 00 01 00 68";
            String CMD104_DATA = "AA F5 F3 00 10 01 68 00 00 00 00 00 33 31 30 31 30 34 30 30 30 31 30 30 30 30 32 30 30 30 30 34 00 00 00 00 00 00 00 00 00 00 00 00 01 01 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 8E 17 8A 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 87 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0A 0A 0A 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FF 02 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 00 00 00 00 00 00 00 00 01 00 00 14";
            //    String CMD105_DATA = "AA F5 A0 00 04 1C 69 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 20 20 10 12 15 12 56 FF 47";
            String CMD105_DATA = "AA F5 1C 00 00 F8 69 00 00 00 00 00 0A 00 00 F6 00 00 00 20 16 04 28 12 00 00 FF DC";
            String CMD106_DATA = "AA F5 7E 00 10 1C 6A 00 00 00 00 00 33 31 30 31 30 34 30 30 30 31 30 30 30 30 32 30 30 30 30 34 00 00 00 00 00 00 00 00 00 00 00 00 00 09 52 00 00 01 00 64 00 00 00 01 1E 00 00 01 03 02 01 00 00 00 20 20 10 12 15 12 55 FF 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 CE 01 00 00 00 00 00 30 30 30 30 32 31 30 30 31 30 31 32 00 00 00 00 13";
            String CMD108_DATA = "AA F5 4D 00 10 06 6C 00 00 00 00 00 33 31 30 31 30 34 30 30 30 31 30 30 30 30 32 30 30 30 30 34 00 00 00 00 00 00 00 00 00 00 00 00 02 00 04 00 00 00 00 00 00 00 00 00 20 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 62";
            String CMD110_DATA = "AA F5 88 00 10 0E 6E 00 00 00 00 00 30 31 30 30 30 30 31 30 30 30 30 30 30 31 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 01 00 00 00 00 00 00 00 01 01 00 01 E8 03 00 00 EE 02 00 00 00 00 00 00 00 00 00 00 C1 07 00 00 00 00 00 00 00 00 4C 41 42 4C 43 31 32 33 34 35 36 31 32 33 34 35 36 00 00 00 00 00 00 00 00 F4 01 00 00 A0 0F 00 00 4C 1D 00 00 E8 03 00 00 4C F4 01 42 0E 00 00 0A";

            String CMD302_DATA = "AA F5 CC 00 10 08 2E 01 00 00 00 00 04 00 01 00 30 31 30 30 30 30 31 30 30 30 30 30 30 31 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 02 01 01 00 01 E8 03 00 00 EE 02 00 00 00 00 00 00 00 00 00 00 C1 07 00 00 00 00 00 00 00 00 4C 41 42 4C 43 31 32 33 34 35 36 31 32 33 34 35 36 00 00 00 00 00 00 00 00 F4 01 00 00 A0 0F 00 00 4C 1D 00 00 E8 03 00 00 4C F4 01 42 0E 00 00 AA 2C 1A 00 00 A0 0F 00 00 02 00 00 00 00 00 00 00 00 C2 01 00 00 00 F4 01 1D 00 00 00 01 42 02 3D 03 00 00 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 CE";
            String CMD304_DATA = "AA F5 FF 00 10 16 30 01 00 00 00 00 30 31 30 30 30 30 31 30 30 30 30 30 30 31 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 02 01 01 00 01 E8 03 EE 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 4C 41 42 4C 43 31 32 33 34 35 36 31 32 33 34 35 36 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 01 00 01 E8 03 EE 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 4C 41 42 4C 43 31 32 33 34 35 36 31 32 33 34 35 36 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 F4 01 00 00 E8 03 4C 1D 4C F4 01 42 0E 00 00 00 AA 00 00 00 00 00 00 00 2C 1A 00 00 02 00 00 00 00 00 A0 0F C2 01 33 1D 00 00 00 00 00 00 00 00 01 42 02 3D 03 00 10 FF 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 46";

            byte[] fullData = Decoder.hexStringToByteArray(CMD108_DATA.replace(" ", ""));
            buff.writeBytes(fullData);
            client.channel.writeAndFlush(buff);

            System.out.println("......发送 Cmd 成功......");

            TimeUnit.SECONDS.sleep(5000000);

        }
    }
}